<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>座位表编排工具 v9</title>
  <script>
    // 全局错误捕获：把语法/运行错误写到调试面板
    (function(){
      function appendDebug(txt){
        try{ var el = document.getElementById('debug-log'); if(!el) return; var line = document.createElement('div'); line.textContent = txt; el.appendChild(line);}catch(e){}
      }
      window.addEventListener('error', function(e){ appendDebug('Error: ' + (e.message||e.error)); });
      window.addEventListener('unhandledrejection', function(e){ var r = e && e.reason; var msg = (r && (r.message||r)) || 'Unknown rejection'; appendDebug('Unhandled Rejection: ' + msg); });
    })();
  </script>
  <script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script>
  <style>
    :root{ --bg:#0b0c10; --panel:#12131a; --muted:#a7b0c0; --accent:#4fa3ff; --card:#161821; --border:#232532; --male:#7fc8ff; --female:#ff9ec0; --unknown:#9aa5b1; }
    body{ margin:0; font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "PingFang SC", "Microsoft YaHei", sans-serif; background: var(--bg); color:#e9eef5; }
    .container{ max-width:1260px; margin:18px auto 80px; padding:0 16px; }
    header{ display:flex; align-items:center; justify-content:space-between; gap:12px; margin-bottom:12px; }
    h1{ font-size:22px; margin:0; }
    .sub{ color:var(--muted); font-size:13px; }
    .legend{ display:flex; align-items:center; gap:8px; color:var(--muted); font-size:12px; }
    .dot{ width:10px; height:10px; border-radius:50%; display:inline-block; }
    .stack{ display:grid; grid-template-columns:1fr; gap:14px; }
    .card{ background:linear-gradient(180deg, rgba(255,255,255,.02), rgba(255,255,255,0)); border:1px solid var(--border); border-radius:16px; padding:12px; }
    .toolbar{ display:flex; gap:8px; align-items:center; margin-bottom:8px; flex-wrap:wrap; }
    .btn{ appearance:none; border:1px solid var(--border); background:linear-gradient(180deg, rgba(255,255,255,.05), rgba(255,255,255,0)); color:#e9eef5; padding:8px 12px; border-radius:10px; cursor:pointer; font-weight:600; font-size:13px; }
    .btn.pri{ border-color:rgba(79,163,255,.5); background:linear-gradient(180deg, rgba(79,163,255,.18), rgba(79,163,255,.05)); }
    .btn.sec{ border-color:rgba(143,107,255,.5); background:linear-gradient(180deg, rgba(143,107,255,.18), rgba(143,107,255,.05)); }
    .btn.ghost{ background:transparent; border-style:dashed; }
    .controls{ display:grid; grid-template-columns:1fr; gap:10px; }
    .controls .line{ display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
    input[type=file]{ display:none; }
    .pill{ padding:6px 10px; border-radius:999px; font-size:12px; border:1px solid var(--border); color:#cfd6e4; }
    .stats{ display:grid; grid-template-columns: repeat(4,minmax(0,1fr)); gap:10px; }
    .stat{ background:var(--card); border:1px solid var(--border); border-radius:12px; padding:10px 12px; }
    .stat .k{ color:var(--muted); font-size:12px; }
    .stat .v{ font-size:18px; font-weight:700; margin-top:2px; }
    .grid-wrap{ overflow:auto; }
    table.grid{ width:100%; border-collapse:separate; border-spacing:0; min-width:1000px; }
    .grid th{ position:sticky; top:0; background:#0c0e14; border-bottom:1px solid var(--border); z-index:1; }
    .grid th,.grid td{ padding:9px 12px; font-size:15px; text-align:left; }
    .grid td{ border-bottom:1px dashed #1f2230; }
    .col-head{ font-weight:700; color:#cdd6e0; }
    .seat{ position:relative; padding-left:14px; cursor:grab; }
    .seat.male::before{ content:""; position:absolute; left:2px; top:50%; transform:translateY(-50%); width:9px; height:9px; border-radius:50%; background:var(--male); }
    .seat.female::before{ content:""; position:absolute; left:2px; top:50%; transform:translateY(-50%); width:9px; height:9px; border-radius:50%; background:var(--female); }
    .seat.unknown::before{ content:""; position:absolute; left:2px; top:50%; transform:translateY(-50%); width:9px; height:9px; border-radius:50%; background:var(--unknown); }
    .ghost{ color:#8c93a3; opacity:.7; font-style:italic; }
    .ops{ display:inline-flex; gap:6px; opacity:0; margin-left:8px; }
    td:hover .ops{ opacity:.85; }
    .op-mini{ border:1px solid var(--border); padding:2px 6px; border-radius:8px; font-size:11px; cursor:pointer; background:rgba(255,255,255,.03); }
    .seatno{ color:#c5ccda; text-align:center !important; font-variant-numeric:tabular-nums; width:60px; }
    td.sel{ outline:2px solid rgba(79,163,255,.7); outline-offset:-2px; background:rgba(79,163,255,.08); }
    .unplaced-area{ margin-top:12px; padding:10px; border: 1px dashed var(--border); border-radius: 12px; min-height: 40px; }
    .unplaced-title{ font-size:13px; color:var(--muted); margin-bottom:6px; }
    .unplaced-list{ display:flex; flex-wrap:wrap; gap:6px; }
    .unplaced-item{ padding:4px 8px; border-radius:8px; background:var(--card); border:1px solid var(--border); cursor:grab; font-size:13px; }
    .warn-box{ background:linear-gradient(180deg, rgba(255,176,32,.08), rgba(255,176,32,.02)); border:1px solid rgba(255,176,32,.3); padding:10px 12px; border-radius:10px; font-size:13px; color:#ffd996; }
    .ok-box{ background:linear-gradient(180deg, rgba(29,185,84,.08), rgba(29,185,84,.02)); border:1px solid rgba(29,185,84,.3); padding:10px 12px; border-radius:10px; font-size:13px; color:#b8f5c5; }
    .debug{ background:linear-gradient(180deg, rgba(255,255,255,.02), rgba(255,255,255,0)); border:1px dashed var(--border); border-radius:14px; padding:10px 12px; font-size:12px; color:#d6def0; max-height:220px; overflow:auto; }
    .debug-title{ font-weight:700; font-size:12px; color:#94a0b4; margin-bottom:6px; display:flex; align-items:center; justify-content:space-between; }
  </style>
</head>
<body>
  <div class="container">
    <header>
      <div>
        <h1>座位表编排工具 v9</h1>
        <div class="sub">上方展示座位表，下方进行导入/展示/编排/导出与配置。模板包含两个工作表：<b>座位表</b>（含“座号”栏）与<b>学生名单</b>（姓名｜性别｜坐后面次数）。</div>
      </div>
      <div class="legend">
        <span class="dot" style="background:var(--male);"></span><span>男生</span>
        <span class="dot" style="background:var(--female);"></span><span>女生</span>
      </div>
    </header>

    <div class="stack">
      <section class="card">
        <div class="toolbar">
          <button class="btn" id="btn-undo">↩️ 撤销</button>
          <button class="btn" id="btn-redo">↪️ 重做</button>
          <span style="width:1px;height:20px;background:#2a2d3a"></span>
          <button class="btn" id="btn-clear">⌫ 批量清空</button>
          <button class="btn" id="btn-cut">✂️ 剪切</button>
          <button class="btn" id="btn-paste">📋 粘贴</button>
          <button class="btn ghost" id="btn-cancel">取消选择</button>
          <span style="margin-left:auto; font-size:12px; color:#9fb0c8;" id="sel-hint">未选择</span>
        </div>
        <div class="grid-wrap"><table class="grid" id="grid"></table></div>
        <div class="unplaced-area" id="unplaced-area" style="display:none;">
          <div class="unplaced-title">未安排的学生 (可将学生拖拽至此区域)</div>
          <div class="unplaced-list" id="unplaced-list"></div>
        </div>
      </section>

      <section class="card">
        <div class="controls">
          <div class="line">
            <label for="file" class="btn pri">📥 选择 Excel 文件</label>
            <input id="file" type="file" accept=".xlsx,.xls" />
            <button class="btn" id="btn-present" disabled>👀 展示当前座位</button>
            <button class="btn" id="btn-rebuild" disabled>⚙️ 重新编排</button>
            <button class="btn sec" id="btn-sample">📄 下载模板</button>
            <button class="btn" id="btn-add-stu">➕ 添加学生</button>
          </div>
          <div class="line" style="gap:6px;">
            <span class="pill">学生名单表头：姓名｜性别｜坐后面次数（或 name｜gender｜behindCount）</span>
            <span class="pill">性别可填：男/女、M/F、Male/Female</span>
          </div>
          <div class="line">
            <label><input type="checkbox" id="auto-count"/> 自动更新“坐后面次数”（基于当前前后排配对）</label>
            <label style="margin-left:12px;"><input type="checkbox" id="show-order" checked/> 以 5-1-2-6-7-3-4-8 顺序展示</label>
          </div>
          <div class="line">
            <label>后排最小间隔：
              <select id="cfg-gap" class="btn">
                <option value="0">允许相邻（0）</option>
                <option value="1" selected>至少隔 1 位（1）</option>
                <option value="2">至少隔 2 位（2）</option>
              </select>
            </label>
            <label>后排性别偏好：
              <select id="cfg-pref" class="btn">
                <option value="none" selected>无</option>
                <option value="M">男优先</option>
                <option value="F">女优先</option>
              </select>
            </label>
          </div>
          <div id="info" class="ok-box" style="display:none;"></div>
          <div class="stats" id="stats" style="display:none;">
            <div class="stat"><div class="k">总人数</div><div class="v" id="stat-total">-</div></div>
            <div class="stat"><div class="k">男 / 女</div><div class="v" id="stat-mf">-</div></div>
            <div class="stat"><div class="k">剩余（>56）</div><div class="v" id="stat-left">-</div></div>
            <div class="stat"><div class="k">模式</div><div class="v" id="stat-mode">-</div></div>
          </div>
          <div class="line"><button class="btn" id="btn-export" disabled>📤 导出（覆盖模板结构）</button></div>
          <div id="warn" class="warn-box" style="display:none;"></div>
          <div class="debug" id="debug">
            <div class="debug-title"><span>🪵 调试面板（捕获页面错误/测试输出）</span><button class="btn" id="btn-clear-log" style="padding:4px 8px; font-size:12px;">清空</button></div>
            <div id="debug-log"></div>
          </div>
        </div>
      </section>
    </div>
  </div>

<script>
(function(){
  // ====== 常量 / 状态 ======
  const M='M', F='F';
  const FRONT_COLS=4, ROWS_PER_COL=14;
  let cfg = { behindMinGap: 1, behindGenderPref: 'none' };

  let baseStudents=[]; // 从“学生名单”读取
  let students=[];     // 工作副本
  let seating=null;    // { frontCols, behindCols, unplaced, plannedBehind, mode }
  let currentView=[];  // 渲染列定义

  const history=[]; const future=[];
  function snapshot(){ return JSON.parse(JSON.stringify({ seating, baseStudents, students })); }
  function pushHistory(){ if(!seating) return; history.push(snapshot()); future.length=0; updateUndoRedoBtns(); }
  function undo(){ if(!history.length) return; future.push(snapshot()); const state=history.pop(); seating=state.seating; baseStudents=state.baseStudents; students=state.students; renderGrid(seating); updateStatsFromMaster(); updateUndoRedoBtns(); }
  function redo(){ if(!future.length) return; history.push(snapshot()); const state=future.pop(); seating=state.seating; baseStudents=state.baseStudents; students=state.students; renderGrid(seating); updateStatsFromMaster(); updateUndoRedoBtns(); }
  function updateUndoRedoBtns(){ btnUndo.disabled = history.length===0; btnRedo.disabled = future.length===0; }

  // ====== 句柄 ======
  const fileInput=document.getElementById('file');
  const btnPresent=document.getElementById('btn-present');
  const btnRebuild=document.getElementById('btn-rebuild');
  const btnExport=document.getElementById('btn-export');
  const btnSample=document.getElementById('btn-sample');
  const btnAddStu=document.getElementById('btn-add-stu');
  const btnClearLog=document.getElementById('btn-clear-log');
  const grid=document.getElementById('grid');
  const unplacedArea=document.getElementById('unplaced-area');
  const unplacedList=document.getElementById('unplaced-list');
  const info=document.getElementById('info');
  const warn=document.getElementById('warn');
  const statTotal=document.getElementById('stat-total');
  const statMF=document.getElementById('stat-mf');
  const statLeft=document.getElementById('stat-left');
  const statMode=document.getElementById('stat-mode');
  const statsBox=document.getElementById('stats');
  const debugLog=document.getElementById('debug-log');
  const autoCount=document.getElementById('auto-count');
  const showOrder=document.getElementById('show-order');
  const btnUndo=document.getElementById('btn-undo');
  const btnRedo=document.getElementById('btn-redo');
  const btnClearSel=document.getElementById('btn-clear');
  const btnCut=document.getElementById('btn-cut');
  const btnPaste=document.getElementById('btn-paste');
  const btnCancel=document.getElementById('btn-cancel');
  const selHint=document.getElementById('sel-hint');
  const cfgGap=document.getElementById('cfg-gap');
  const cfgPref=document.getElementById('cfg-pref');

  let importedSeatingSheetAOA=null;
  function log(msg){ const d=document.createElement('div'); d.textContent=String(msg); debugLog.appendChild(d); }
  function clearLog(){ debugLog.innerHTML=''; }

  // ====== 事件 ======
  fileInput.addEventListener('change', async (e)=>{
    const file=e.target.files && e.target.files[0];
    resetUI(); if(!file) return;
    const ab=await file.arrayBuffer();
    const wb=XLSX.read(ab,{type:'array'});
    const hasRoster=wb.SheetNames.includes('学生名单');
    const rosterWS=wb.Sheets[hasRoster?'学生名单':wb.SheetNames[0]];
    const rosterJSON=XLSX.utils.sheet_to_json(rosterWS,{defval:''});
    baseStudents=parseStudents(rosterJSON);
    students=baseStudents.map((s,i)=>({...s,index:i}));

    importedSeatingSheetAOA=null;
    if(wb.SheetNames.includes('座位表')){
      importedSeatingSheetAOA = XLSX.utils.sheet_to_json(wb.Sheets['座位表'],{header:1,defval:''});
    }

    if(baseStudents.length===0){ showWarn('没有在“学生名单”表读取到有效数据。请检查表头是否为「姓名｜性别｜坐后面次数」。'); return; }
    info.style.display='block'; info.textContent=`已加载：${file.name}（学生名单 ${baseStudents.length} 行${importedSeatingSheetAOA?'，含座位表':''}）`;
    updateStatsFromMaster();
    btnPresent.disabled=!importedSeatingSheetAOA; btnRebuild.disabled=false;
  });

  btnPresent.addEventListener('click',()=>{
    try{ seating=buildSeatingFromSheet(importedSeatingSheetAOA, students); seating.mode='present'; history.length=0; future.length=0; renderGrid(seating); btnExport.disabled=false; statMode.textContent='展示当前座位'; updateUndoRedoBtns(); log('✅ 已根据“座位表”工作表呈现'); }catch(err){ console.error(err); log('❌ 展示失败：'+(err.message||err)); showWarn('展示失败：'+(err.message||err)); }
  });

  btnRebuild.addEventListener('click',()=>{
    try{ seating=buildSeatingRecompute(students); seating.mode='rebuild'; history.length=0; future.length=0; renderGrid(seating); btnExport.disabled=false; statMode.textContent='重新编排'; updateUndoRedoBtns(); log('✅ 重新编排完成'); }catch(err){ console.error(err); log('❌ 编排失败：'+(err.message||err)); showWarn('编排失败：'+(err.message||err)); }
  });
  
  btnAddStu.addEventListener('click', ()=>{
    const name = prompt('请输入新增学生的姓名：');
    if(!name || !name.trim()) return;
    const genderRaw = prompt(`请输入 ${name.trim()} 的性别（男/女）：`);
    if(!genderRaw) return;
    const gender = normalizeGender(genderRaw);
    if(!gender){ alert('性别无法识别，请输入“男”或“女”。'); return; }
    
    if(seating) pushHistory();
    const newStudent = { name: name.trim(), gender, count: 0, index: baseStudents.length };
    baseStudents.push(newStudent);
    students.push(newStudent);
    
    if(seating){
      seating.unplaced.push(newStudent);
      renderGrid(seating);
    }
    updateStatsFromMaster();
    log(`➕ 已添加学生：${newStudent.name} (${newStudent.gender})`);
  });

  function updateStats(total,m,f,mode){
    statTotal.textContent=String(total); statMF.textContent=`${m} / ${f}`; statLeft.textContent=total>56? String(total-56):'0'; statMode.textContent=mode; statsBox.style.display='grid';
  }
  function updateStatsFromMaster(){
    const total = baseStudents.length;
    const m = baseStudents.filter(s=>s.gender===M).length;
    const f = total - m;
    updateStats(total, m, f, seating ? seating.mode : '-');
  }

  btnExport.addEventListener('click',()=>{ if(!seating) return; exportExcelTwoSheets(seating); log('📤 已导出覆盖模板结构文件'); });
  btnSample.addEventListener('click', downloadTemplateTwoSheets);
  btnClearLog.addEventListener('click', clearLog);
  showOrder.addEventListener('change',()=>{ if(seating) renderGrid(seating); });
  cfgGap.addEventListener('change',()=>{ cfg.behindMinGap=parseInt(cfgGap.value,10)||0; if(seating && seating.mode==='rebuild'){ pushHistory(); seating=buildSeatingRecompute(students); seating.mode='rebuild'; renderGrid(seating);} });
  cfgPref.addEventListener('change',()=>{ cfg.behindGenderPref=cfgPref.value; if(seating && seating.mode==='rebuild'){ pushHistory(); seating=buildSeatingRecompute(students); seating.mode='rebuild'; renderGrid(seating);} });

  btnUndo.addEventListener('click',undo); btnRedo.addEventListener('click',redo);
  btnClearSel.addEventListener('click',()=>{ if(!selection.cells.length) return; pushHistory(); selection.cells.forEach(ref=>{ setCell(ref,null); }); clearSelection(); renderGrid(seating); });
  btnCut.addEventListener('click',()=>{ if(!selection.cells.length) return; clipboard=captureSelection(selection); pushHistory(); selection.cells.forEach(ref=>setCell(ref,null)); renderGrid(seating); pasteMode=true; selHint.textContent=`已剪切 ${clipboard.items.length} 个座位：点击目标单元格以粘贴`; });
  btnPaste.addEventListener('click',()=>{ if(!clipboard.items.length) return; pasteMode=true; selHint.textContent=`粘贴模式：点击目标单元格以粘贴 ${clipboard.items.length} 个座位`; });
  btnCancel.addEventListener('click',()=>{ clearSelection(); pasteMode=false; selHint.textContent='未选择'; });

  unplacedArea.ondragover = (e) => { e.preventDefault(); };
  unplacedArea.ondrop = (e) => {
    e.preventDefault();
    if (!dragSrc || dragSrc.type !== 'seat') { dragSrc = null; return; }
    pushHistory();
    const studentToMove = getCell(dragSrc);
    if (studentToMove) {
      seating.unplaced.push(studentToMove);
      setCell(dragSrc, null);
    }
    renderGrid(seating);
    dragSrc = null;
  };

  function resetUI(){ info.style.display='none'; warn.style.display='none'; warn.textContent=''; grid.innerHTML=''; unplacedArea.style.display='none'; unplacedList.innerHTML=''; statsBox.style.display='none'; btnPresent.disabled=true; btnRebuild.disabled=true; btnExport.disabled=true; seating=null; baseStudents=[]; students=[]; importedSeatingSheetAOA=null; history.length=0; future.length=0; updateUndoRedoBtns(); clearSelection(); }
  function showWarn(msg){ warn.style.display='block'; warn.textContent=msg; }

  // ====== 解析“学生名单” ======
  const normalizeGender = (g)=>{ const t=String(g).trim().toLowerCase(); if(['m','male','男','♂','boy'].includes(t)) return M; if(['f','female','女','♀','girl'].includes(t)) return F; return ''; };
  function parseStudents(json){
    const mapHeader = (h)=>String(h).trim().toLowerCase();
    const row0=json[0]||{};
    const nameKey=Object.keys(row0).find(k=>['姓名','name'].includes(mapHeader(k)))||'姓名';
    const genderKey=Object.keys(row0).find(k=>['性别','gender'].includes(mapHeader(k)))||'性别';
    const countKey=Object.keys(row0).find(k=>['坐后面次数','behindcount','次数','count'].includes(mapHeader(k)))||'坐后面次数';
    const out=[]; json.forEach((r,idx)=>{ const name=String(r[nameKey]??'').trim(); const gender=normalizeGender(r[genderKey]); let count=Number(r[countKey]); if(!Number.isFinite(count)) count=0; if(!name||!gender) return; out.push({name,gender,count,index:idx}); });
    return out;
  }

  // ====== 从“座位表”读取（含座号列） ======
  function buildSeatingFromSheet(aoa, roster){
    if(!aoa||!aoa.length) throw new Error('座位表为空');
    const header=(aoa[0]||[]).map(String); const dataRows=aoa.slice(1);
    const frontCols=Array.from({length:FRONT_COLS},()=>Array(ROWS_PER_COL).fill(null));
    const behindCols=Array.from({length:FRONT_COLS},()=>Array(ROWS_PER_COL).fill(null));
    const rosterMap=new Map(); roster.forEach(s=>{ if(!rosterMap.has(s.name)) rosterMap.set(s.name,s); });
    const colMeta=header.map((h,j)=>{ const h2=String(h).trim(); if(/座号/.test(h2)) return {kind:'seatno'}; const isBehind=/后排/.test(h2); const m=h2.match(/(\d+)/); const num=m?parseInt(m[1],10):(j+1); let baseIndex=(num<=4)? num-1 : (num-1-4); if(baseIndex<0||baseIndex>3) { log(`警告: 无法解析列 "${h}" 的索引, 已忽略`); return null; } return { kind: isBehind?'behind':'front', col: baseIndex }; });
    const placedStudents=new Set();
    for(let r=0;r<ROWS_PER_COL;r++){
      for(let j=0;j<header.length;j++){
        const meta=colMeta[j]; if(!meta||meta.kind==='seatno') continue;
        const cell=(dataRows[r] && dataRows[r][j])? String(dataRows[r][j]).trim():''; if(!cell) continue;
        const stu=rosterMap.get(cell);
        if(!stu) { log(`警告: 座位表中的学生 "${cell}" 不在学生名单中`); continue; }
        placedStudents.add(stu.name);
        if(meta.kind==='behind') behindCols[meta.col][r]=stu; else frontCols[meta.col][r]=stu;
      }
    }
    const unplaced = roster.filter(s=>!placedStudents.has(s.name));
    return { frontCols, behindCols, unplaced, plannedBehind:[], mode:'present' };
  }

  // ====== 重新编排 ======
  // --- BUG FIX in v9 ---
  function selectBehindFirst(source) {
    const total = source.length;
    const need = Math.max(0, total - 56);
    if (need <= 0) return [];

    const sortedSource = [...source];
    sortedSource.sort((a, b) => {
        if (a.count !== b.count) {
            return a.count - b.count; // Prioritize lower count
        }
        return a.index - b.index; // Stable sort using original index
    });

    return sortedSource.slice(0, need);
  }

  function buildSeatingRecompute(source){
    if(!Array.isArray(source)||source.length===0) throw new Error('数据为空');
    
    // Use the corrected function to select students for the back row
    const plannedBehind=selectBehindFirst(source);
    
    const keys=new Set(plannedBehind.map(s=>s.name+'|'+s.index));
    const frontCandidates = source.filter(s=>!keys.has(s.name+'|'+s.index))
                                  .sort((a,b)=> (b.count!==a.count)?(b.count-a.count):(a.index-b.index)); // Prioritize higher count for front seats

    const males=frontCandidates.filter(s=>s.gender===M); const females=frontCandidates.filter(s=>s.gender===F);
    const frontCols=Array.from({length:FRONT_COLS},()=>Array(ROWS_PER_COL).fill(null));
    const maleShortageCols=[];
    for(let c=0;c<FRONT_COLS;c++){
      for(let r=0;r<ROWS_PER_COL;r++){
        const expect=(r%2===0)?M:F; let pick=null;
        if(expect===M && males.length) pick=males.shift(); else if(expect===F && females.length) pick=females.shift(); else if(males.length) pick=males.shift(); else if(females.length) pick=females.shift();
        frontCols[c][r]=pick||null;
      }
      if(!frontCols[c][0] || frontCols[c][0].gender!==M) maleShortageCols.push(c+1);
    }

    const behindCols=Array.from({length:FRONT_COLS},()=>Array(ROWS_PER_COL).fill(null));
    let behindM=plannedBehind.filter(s=>s.gender===M);
    let behindF=plannedBehind.filter(s=>s.gender===F);
    const colQueues=Array.from({length:FRONT_COLS},()=>({ maleRows:[], femaleRows:[], iM:0, iF:0, last:-999 }));
    for(let c=0;c<FRONT_COLS;c++){
      const maleRows=[]; const femaleRows=[];
      for(let r=0;r<ROWS_PER_COL;r++){
        const host=frontCols[c][r]; if(!host) continue; (host.gender===M?maleRows:femaleRows).push(r);
      }
      colQueues[c].maleRows=maleRows; colQueues[c].femaleRows=femaleRows;
    }

    function nextRowGE(arr,startIdx,minRow){ for(let i=startIdx;i<arr.length;i++){ if(arr[i]>=minRow) return i; } return -1; }

    while(behindM.length || behindF.length){
      let placedAny=false;
      for(let c=0;c<FRONT_COLS;c++){
        const q=colQueues[c]; const minRow=q.last + (cfg.behindMinGap + 1);
        const idxM = (behindM.length? nextRowGE(q.maleRows, q.iM, minRow) : -1);
        const idxF = (behindF.length? nextRowGE(q.femaleRows, q.iF, minRow) : -1);
        if(idxM===-1 && idxF===-1) continue;
        let pickGender=null; let pickRow=Infinity; let useIdx=-1;
        if(idxM!==-1 && idxF!==-1){
          const scoreM = behindM.length + (cfg.behindGenderPref==='M'?0.5:0);
          const scoreF = behindF.length + (cfg.behindGenderPref==='F'?0.5:0);
          if(scoreM>scoreF){ pickGender=M; pickRow=q.maleRows[idxM]; useIdx=idxM; }
          else if(scoreF>scoreM){ pickGender=F; pickRow=q.femaleRows[idxF]; useIdx=idxF; }
          else { if(q.maleRows[idxM] <= q.femaleRows[idxF]){ pickGender=M; pickRow=q.maleRows[idxM]; useIdx=idxM; } else { pickGender=F; pickRow=q.femaleRows[idxF]; useIdx=idxF; } }
        } else if(idxM!==-1){ pickGender=M; pickRow=q.maleRows[idxM]; useIdx=idxM; }
        else { pickGender=F; pickRow=q.femaleRows[idxF]; useIdx=idxF; }

        if(pickGender===M && behindM.length){ behindCols[c][pickRow]={ ...behindM.shift(), behindAssigned:true }; q.iM=useIdx+1; q.last=pickRow; placedAny=true; }
        else if(pickGender===F && behindF.length){ behindCols[c][pickRow]={ ...behindF.shift(), behindAssigned:true }; q.iF=useIdx+1; q.last=pickRow; placedAny=true; }
      }
      if(!placedAny) break;
    }

    const warnings=[]; if(maleShortageCols.length){ warnings.push(`以下列无法以男生开头（数据中男生不足）：第 ${maleShortageCols.join('、')} 列`); }
    if(warnings.length) showWarn(warnings.join('\n'));

    return { frontCols, behindCols, unplaced:[...behindM,...behindF], plannedBehind, mode:'rebuild' };
  }

  // ====== 渲染 & 交互 ======
  function makeSeatNumbers(){
    const a=Array.from({length:14},(_,i)=>43+i); const b=Array.from({length:14},(_,i)=>42-i);
    const c=Array.from({length:14},(_,i)=>15+i); const d=Array.from({length:14},(_,i)=>14-i);
    return [a,b,c,d];
  }
  const seatNos=makeSeatNumbers();

  function renderGrid(seat){
    const useStandardOrder=!!showOrder.checked;
    currentView = useStandardOrder ? [
      {t:'behind',c:0,label:'列5(后排)'}, {t:'front', c:0,label:'列1'}, {t:'seatno',c:0,label:'座号'}, {t:'seatno',c:1,label:'座号'}, {t:'front', c:1,label:'列2'}, {t:'behind',c:1,label:'列6(后排)'},
      {t:'behind',c:2,label:'列7(后排)'}, {t:'front', c:2,label:'列3'}, {t:'seatno',c:2,label:'座号'}, {t:'seatno',c:3,label:'座号'}, {t:'front', c:3,label:'列4'}, {t:'behind',c:3,label:'列8(后排)'}
    ] : [
      {t:'front', c:0,label:'列1'}, {t:'seatno',c:0,label:'座号'}, {t:'behind',c:0,label:'列5(后排)'}, {t:'front', c:1,label:'列2'}, {t:'seatno',c:1,label:'座号'}, {t:'behind',c:1,label:'列6(后排)'},
      {t:'front', c:2,label:'列3'}, {t:'seatno',c:2,label:'座号'}, {t:'behind',c:2,label:'列7(后排)'}, {t:'front', c:3,label:'列4'}, {t:'seatno',c:3,label:'座号'}, {t:'behind',c:3,label:'列8(后排)'}
    ];

    grid.innerHTML='';
    const head=document.createElement('tr');
    currentView.forEach(item=>{ const th=document.createElement('th'); th.className='col-head'; th.textContent=item.label; head.appendChild(th); });
    grid.appendChild(head);

    for(let r=0;r<ROWS_PER_COL;r++){
      const tr=document.createElement('tr');
      currentView.forEach((item,vcol)=>{
        if(item.t==='seatno'){ const td=document.createElement('td'); td.className='seatno'; td.textContent=seatNos[item.c][r]; td.dataset.kind='seatno'; td.dataset.vcol=String(vcol); tr.appendChild(td); return; }
        const s=item.t==='front' ? seat.frontCols[item.c][r] : seat.behindCols[item.c][r];
        const td=seatCell(s,item.t,item.c,r,vcol); tr.appendChild(td);
      });
      grid.appendChild(tr);
    }
    
    unplacedList.innerHTML = '';
    unplacedArea.style.display = 'block'; // Always show the area as a drop target
    if(seat.unplaced && seat.unplaced.length > 0){
      seat.unplaced.forEach((stu,idx) => {
        const item = document.createElement('div');
        item.className = 'unplaced-item seat ' + (stu.gender===M ? 'male' : (stu.gender===F ? 'female' : 'unknown'));
        item.textContent = stu.name;
        item.draggable = true;
        item.ondragstart = (e) => { onDragStartUnplaced(e, idx); };
        unplacedList.appendChild(item);
      });
    }
    attachGridSelectionHandlers();
  }

  function seatCell(stu, area, col, row, vcol){
    const td=document.createElement('td');
    td.dataset.area=area; td.dataset.col=String(col); td.dataset.row=String(row); td.dataset.vcol=String(vcol); td.dataset.kind='seat';
    td.ondragover=(e)=>{ e.preventDefault(); };
    td.ondrop=onDrop;
    if(!stu){ td.innerHTML='<span class="ghost">—</span>'; return td; }
    td.draggable=true; td.ondragstart=onDragStartSeat;
    const cls = stu.gender===M ? 'male' : (stu.gender===F ? 'female' : 'unknown');
    const delBtn = '<span class="ops"><button class="op-mini" data-op="del">× 删除</button></span>';
    td.innerHTML = '<span class="seat '+cls+'">'+escapeHtml(stu.name)+'</span>'+delBtn;
    td.onclick=(e)=>{ 
      const t=e.target; 
      if(t && t.dataset && t.dataset.op==='del'){ 
        pushHistory(); 
        const removed = getCell({area,col,row}); 
        setCell({area,col,row}, null); 
        if(removed) {
          baseStudents = baseStudents.filter(s => s.index !== removed.index);
          students = students.filter(s => s.index !== removed.index);
        }
        renderGrid(seating);
        updateStatsFromMaster();
      } else if(pasteMode && clipboard.items.length){ 
        pasteTo({vcol:+td.dataset.vcol,row}); 
      } 
    };
    return td;
  }

  function getCell(ref){ return ref.area==='front' ? seating.frontCols[ref.col][ref.row] : seating.behindCols[ref.col][ref.row]; }
  function setCell(ref,val){ if(ref.area==='front'){ seating.frontCols[ref.col][ref.row]=val; } else { seating.behindCols[ref.col][ref.row]=val; } }

  let dragSrc=null; 
  function onDragStartSeat(e){ const td=e.currentTarget; dragSrc={ type:'seat', area:td.dataset.area, col:+td.dataset.col, row:+td.dataset.row }; try{ e.dataTransfer.setData('text/plain', JSON.stringify(dragSrc)); }catch(_){ } }
  function onDragStartUnplaced(e, index){ dragSrc={ type: 'unplaced', index }; try{ e.dataTransfer.setData('text/plain', JSON.stringify(dragSrc)); }catch(_){ } }
  
  function onDrop(e){ 
    try{ e.preventDefault(); }catch(_){ } 
    const td=e.currentTarget; const dst={ area:td.dataset.area, col:+td.dataset.col, row:+td.dataset.row }; 
    if(!dragSrc||!seating) return; 
    pushHistory(); 
    const studentToMove = dragSrc.type === 'seat' ? getCell(dragSrc) : seating.unplaced[dragSrc.index];
    const studentAtDest = getCell(dst);

    setCell(dst, studentToMove);
    if(dragSrc.type === 'seat'){
      setCell(dragSrc, studentAtDest);
    } else {
      seating.unplaced.splice(dragSrc.index, 1);
      if(studentAtDest) seating.unplaced.push(studentAtDest);
    }
    
    renderGrid(seating); 
    dragSrc=null; 
  }

  function escapeHtml(str){ return String(str).replace(/[&<>"']/g, function(ch){ return ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;','\'':'&#39;'}[ch]); }); }

  // ====== 选择 / 批量操作 ======
  let isMouseDown=false; let selection={start:null,end:null,cells:[]}; let clipboard={items:[],width:0,height:0}; let pasteMode=false;
  function attachGridSelectionHandlers(){
    grid.onmousedown=(e)=>{ const td=e.target.closest('td'); if(!td||td.dataset.kind==='seatno') return; isMouseDown=true; selection.start={vcol:+td.dataset.vcol, row:+td.dataset.row}; selection.end={...selection.start}; updateSelection(); };
    grid.onmousemove=(e)=>{ if(!isMouseDown) return; const td=e.target.closest('td'); if(!td||td.dataset.kind==='seatno') return; selection.end={vcol:+td.dataset.vcol, row:+td.dataset.row}; updateSelection(); };
    grid.onmouseup=()=>{ isMouseDown=false; };
    grid.onmouseleave=()=>{ isMouseDown=false; };
  }
  function updateSelection(){ clearSelClass(); selection.cells=[]; const minV=Math.min(selection.start.vcol, selection.end.vcol); const maxV=Math.max(selection.start.vcol, selection.end.vcol); const minR=Math.min(selection.start.row, selection.end.row); const maxR=Math.max(selection.start.row, selection.end.row); for(let r=minR;r<=maxR;r++){ for(let v=minV; v<=maxV; v++){ const meta=currentView[v]; if(!meta||meta.t==='seatno') continue; const td=getTd(v,r); if(!td) continue; td.classList.add('sel'); selection.cells.push({ area:meta.t,col:meta.c,row:r,vcol:v }); } } selHint.textContent=selection.cells.length? ('已选择 '+selection.cells.length+' 个座位') : '未选择'; }
  function clearSelection(){ selection={start:null,end:null,cells:[]}; clearSelClass(); selHint.textContent='未选择'; }
  function clearSelClass(){ grid.querySelectorAll('td.sel').forEach(td=>td.classList.remove('sel')); }
  function getTd(vcol,row){ return grid.querySelector('td[data-vcol="'+vcol+'"][data-row="'+row+'"]'); }

  function captureSelection(sel){ if(!sel.cells.length) return {items:[],width:0,height:0}; const minV=Math.min.apply(null, sel.cells.map(c=>c.vcol)); const minR=Math.min.apply(null, sel.cells.map(c=>c.row)); const maxV=Math.max.apply(null, sel.cells.map(c=>c.vcol)); const maxR=Math.max.apply(null, sel.cells.map(c=>c.row)); const items=[]; for(let r=minR;r<=maxR;r++){ for(let v=minV; v<=maxV; v++){ const meta=currentView[v]; if(!meta||meta.t==='seatno') continue; const ref={ area:meta.t,col:meta.c,row:r,vcol:v }; const stu=getCell(ref); items.push({ dv:v-minV, dr:r-minR, stu }); } } return { items, width:maxV-minV+1, height:maxR-minR+1 }; }
  function pasteTo(anchor){ if(!clipboard.items.length) return; pushHistory(); let placed=0; clipboard.items.forEach(it=>{ const v=anchor.vcol+it.dv; const r=anchor.row+it.dr; const meta=currentView[v]; if(!meta||meta.t==='seatno'||r<0||r>=ROWS_PER_COL) return; const ref={area:meta.t,col:meta.c,row:r}; setCell(ref,it.stu); placed++; }); renderGrid(seating); pasteMode=false; selHint.textContent='已粘贴 '+placed+' 个座位'; }

  // ====== 导出（含“座号”列） ======
  function exportExcelTwoSheets(seat){
    const wb=XLSX.utils.book_new();
    const header=['列5(后排)','列1','座号','座号','列2','列6(后排)','列7(后排)','列3','座号','座号','列4','列8(后排)'];
    const aoa=[header];
    for(let r=0;r<ROWS_PER_COL;r++){
      const row=[];
      row.push(seat.behindCols[0][r]?.name||'');
      row.push(seat.frontCols[0][r]?.name||''); row.push(seatNos[0][r]); row.push(seatNos[1][r]); row.push(seat.frontCols[1][r]?.name||'');
      row.push(seat.behindCols[1][r]?.name||''); row.push(seat.behindCols[2][r]?.name||'');
      row.push(seat.frontCols[2][r]?.name||''); row.push(seatNos[2][r]); row.push(seatNos[3][r]); row.push(seat.frontCols[3][r]?.name||'');
      row.push(seat.behindCols[3][r]?.name||'');
      aoa.push(row);
    }
    XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(aoa),'座位表');

    const finalStudents=computeFinalStudentsForExport(seat);
    const aoa2=[['姓名','性别','坐后面次数']]; finalStudents.forEach(s=>aoa2.push([s.name, s.gender===M?'男':(s.gender===F?'女':''), s.count]));
    XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(aoa2),'学生名单');

    const now=new Date(); const y=now.getFullYear(); const m=String(now.getMonth()+1).padStart(2,'0'); const d=String(now.getDate()).padStart(2,'0');
    XLSX.writeFile(wb, '座位表_'+y+m+d+'.xlsx');
  }

  function computeFinalStudentsForExport(seat){
    const currentStudents = [...seat.frontCols.flat(), ...seat.behindCols.flat(), ...seat.unplaced].filter(Boolean);
    const list = [];
    const seen = new Set();
    for(const s of currentStudents){
        if(!seen.has(s.name + '|' + s.index)){
            list.push({...s});
            seen.add(s.name + '|' + s.index);
        }
    }
    list.sort((a,b)=>a.index-b.index);

    if(!autoCount.checked) return list;

    const pairedStudentNames=new Set();
    for(let c=0;c<FRONT_COLS;c++){ 
      for(let r=0;r<ROWS_PER_COL;r++){ 
        const f=seat.frontCols[c][r];
        const b=seat.behindCols[c][r];
        if(f && b){
          pairedStudentNames.add(f.name);
          pairedStudentNames.add(b.name);
        }
      } 
    }
    
    const used=new Map();
    return list.map(s=>{ 
      const nUsed=used.get(s.name)||0; 
      const shouldPlus=pairedStudentNames.has(s.name) && nUsed===0; 
      used.set(s.name, nUsed + (shouldPlus?1:0)); 
      return shouldPlus? {...s, count:(s.count||0)+1 } : {...s}; 
    });
  }

  // ====== 模板（两个工作表，含座号） ======
  function downloadTemplateTwoSheets(){
    const wb=XLSX.utils.book_new();
    const header=['列5(后排)','列1','座号','座号','列2','列6(后排)','列7(后排)','列3','座号','座号','列4','列8(后排)'];
    const seatAOA=[header]; for(let r=0;r<14;r++) seatAOA.push(['','',seatNos[0][r],seatNos[1][r],'','','','',seatNos[2][r],seatNos[3][r],'','']);
    XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(seatAOA),'座位表');
    const rosterAOA=[['姓名','性别','坐后面次数'],['张三','男',0],['李四','女',0],['王五','男',0]];
    XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(rosterAOA),'学生名单');
    XLSX.writeFile(wb,'座位表模板_两表_含座号_新版.xlsx');
  }

})();
</script>
</body>
</html>